package com.linkallcloud.web.filter;

import java.io.IOException;
import java.time.Duration;
import java.util.List;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.linkallcloud.core.dto.Result;
import com.linkallcloud.core.exception.BizException;
import com.linkallcloud.core.exception.Exceptions;
import com.linkallcloud.core.lang.Mirror;
import com.linkallcloud.core.lang.Strings;
import com.linkallcloud.core.principal.Principal;
import com.linkallcloud.core.www.ISimpleUser;
import com.linkallcloud.core.www.ITokenBox;
import com.linkallcloud.core.www.utils.WebUtils;
import com.linkallcloud.web.utils.Controllers;
import com.linkallcloud.web.utils.IOnlineHelper;

public abstract class AbstractPrincipalFilter<U extends ISimpleUser> extends LacCommonFilter {

    protected Mirror<U> mirror;
    protected ITokenBox tokenBox;
    protected int timeoutMinutes;

    @SuppressWarnings("unchecked")
    public AbstractPrincipalFilter(ITokenBox tokenBox, int timeoutMinutes) {
        super();
        try {
            this.mirror = Mirror.me((Class<U>) Mirror.getTypeParams(getClass())[0]);
        } catch (Throwable e) {
            if (log.isWarnEnabled()) {
                log.warn("!!!Fail to get TypeParams for self!", e);
            }
        }
        this.tokenBox = tokenBox;
        this.timeoutMinutes = timeoutMinutes <= 0 ? 10 : timeoutMinutes;
    }

    @SuppressWarnings("unchecked")
    public AbstractPrincipalFilter(ITokenBox tokenBox, int timeoutMinutes, List<String> ignoreRes, boolean override) {
        super(ignoreRes, override);
        try {
            this.mirror = Mirror.me((Class<U>) Mirror.getTypeParams(getClass())[0]);
        } catch (Throwable e) {
            if (log.isWarnEnabled()) {
                log.warn("!!!Fail to get TypeParams for self!", e);
            }
        }
        this.tokenBox = tokenBox;
        this.timeoutMinutes = timeoutMinutes <= 0 ? 10 : timeoutMinutes;
    }

    public Class<U> getSessionUserClass() {
        return (null == mirror) ? null : mirror.getType();
    }

    protected abstract String getAppCode();

    protected abstract String getLoginUrl();

    protected abstract IOnlineHelper getOnlineHelper();

    protected abstract U getUserByLoginName(int appClazz, String loginName);

    protected U getSessionUserByPrincipal(Principal ssoPrincipal) throws BizException {
        if (ssoPrincipal != null) {
            String loginName = Controllers.getLoginNameByPrincipal(ssoPrincipal);
            U user = getUserByLoginName(ssoPrincipal.getSiteClazz(), loginName);
            if (user != null) {
                return user;
            }
        }
        throw new BizException("e.login", "根据ssoPrincipal加载用户Dto，并转化成SessionUser发生错误");
    }

    @Override
    protected void doConcreteFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        U u = getLoginUser(getAppCode(), request);
        if (null == u) { // suser为空，表示初次登陆或者本地session超时
            // 若有sso, 并且sso未超时, principal为sso验证后的用户帐号
            Principal principal = Controllers.getSSOPrincipal(getAppCode(), request);
            if (principal != null) {// SSO认证，且已经通过SSO认证，但是本地未登陆
                U psu = getSessionUserByPrincipal(principal);
                if (psu != null) {
                    loginUser(request, response, getAppCode(), psu);
                    chain.doFilter(request, response);
                    return;
                }
            }

            // 无有效登录凭证
            if (isIndexOrLocalLoginRequest(request)) {// 访问首页(登陆页面)
                chain.doFilter(request, response);
                return;
            } else {
                toLogin(getLoginUrl(), request, response);
                return;
            }
        }

        chain.doFilter(request, response);
    }

    protected void toLogin(String loginUrl, HttpServletRequest request, HttpServletResponse hResponse)
            throws IOException {
        if (!Strings.isBlank(loginUrl) && !loginUrl.startsWith("http")
                && !loginUrl.startsWith(request.getContextPath())) {
            loginUrl = request.getContextPath() + loginUrl;
        }
        if (WebUtils.isAjax(request)) {
            hResponse.setCharacterEncoding("UTF-8");
            Result<Object> result = Exceptions.makeErrorResult(Exceptions.CODE_ERROR_SESSION_TIMEOUT, "会话超时");
            result.setData(loginUrl);
            // response.sendError(HttpStatus.UNAUTHORIZED.value(), "您已经太长时间没有操作,请刷新页面");
            WebUtils.out(hResponse, result);
        } else {
            hResponse.sendRedirect(loginUrl);
        }
    }

    /**
     * 是否系统首页或者是本地登录的请求，用于非SSO认证过滤时的判断
     *
     * @param request
     * @return boolean
     */
    protected boolean isIndexOrLocalLoginRequest(HttpServletRequest request) {
        String sp = request.getServletPath();
        if (sp.indexOf("/index.html") != -1 || sp.indexOf("/login") != -1) {
            return true;
        }
        return false;
    }

    /**
     * 把用户信息放入session(cookie)中，可以覆盖此方法，在session(cookie)中放入多需要的信息，比如权限
     *
     * @param request
     * @param appCode
     * @param user
     */
    protected void loginUser(HttpServletRequest request, HttpServletResponse response, String appCode, U user) {
        String token = getOnlineHelper().login(user, Duration.ofMinutes(timeoutMinutes));
        Controllers.sendLacTokenCookie(token, request, response);
    }

    /**
     * 取出登录用户
     *
     * @param appCode
     * @param request
     * @return
     */
    protected U getLoginUser(String appCode, HttpServletRequest request) {
        return Controllers.getSessionUser(request, tokenBox, getSessionUserClass());
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    public void init(FilterConfig arg0) throws ServletException {
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.Filter#destroy()
     */
    public void destroy() {
    }

}
